package com.example.JavaWebApi.core.neo4j;

import com.alibaba.fastjson.JSONObject;
import com.example.JavaWebApi.core.neo4j.annonation.EndId;
import com.example.JavaWebApi.core.neo4j.annonation.StartId;
import com.example.JavaWebApi.core.neo4j.annonation.Type;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.neo4j.driver.*;
import org.neo4j.driver.types.Entity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.core.schema.*;
import org.springframework.stereotype.Component;

import javax.validation.constraints.NotNull;
import java.lang.reflect.Field;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @program: java-web-api
 * @description:
 * @author: 杨泽
 * @create: 2023-09-15 13:08
 */
@Component
@Slf4j
public class Neo4jHelper {
    @Autowired
    private Driver driver;
    private String conditionPlaceholder="${queryWrapper.finalConditionCql}";
    private String orderByPlaceholder="${queryWrapper.finalOrderCql}";
    //\$\{([^}]+)}
    //\$\{queryWrapper.objectCql.*\}
    private String objectCqlPlaceholder="\\$\\{queryWrapper.objectCql.([^}]+)}";
    public void executeCqls(List<CQLGroup> groups){
        Session session= driver.session();
        Transaction transaction= session.beginTransaction();
        try{
            for (CQLGroup group:groups){
                String CQL=group.getCQL();
                if(group.getParameter()==null && group.getNeo4jQueryWrapper()==null){
                    runAndLog(transaction,CQL,null);
                }
                else if(group.getParameter()!=null){
                    runAndLog(transaction,CQL,group.getParameter());
                }
                else {
                    group.getNeo4jQueryWrapper().build(0);
                    CQL=CQL.replace(conditionPlaceholder," where "+group.getNeo4jQueryWrapper().getFinalConditionCql())
                            .replace(orderByPlaceholder,group.getNeo4jQueryWrapper().getFinalOrderCql());
                    CQL=replaceObjectCqlParameter(CQL,group.getNeo4jQueryWrapper());
                    runAndLog(transaction,CQL,group.getNeo4jQueryWrapper().getParameters());
                }
            }
            transaction.commit();
        }
        catch (Exception e){
            transaction.rollback();
            log.error(e.getMessage());
            throw new Error(e.getMessage());
        }
        finally {
            transaction.close();
            session.close();
        }

    }
    public void executeCql(String cql, Neo4jQueryWrapper queryWrapper){
        queryWrapper.build(0);
        cql=cql.replace(conditionPlaceholder," where "+queryWrapper.getFinalConditionCql())
                .replace(orderByPlaceholder,queryWrapper.getFinalOrderCql());
        cql=replaceObjectCqlParameter(cql,queryWrapper);
        executeCql(cql,queryWrapper.getParameters());
    }

    public void executeCql(String cql,HashMap<String,Object> parameters){
        Session session= driver.session();
        try{
            runAndLog(session,cql,parameters);
        }
        catch (Exception e){
            log.error(e.getMessage());
            throw new Error(e.getMessage());
        }
        finally {
            session.close();
        }
    }

    /**
     * Description: 手动执行cql
     * @param CQL
     * @param parameters
     * @return java.util.List<T>
     * @author 杨泽
     * @date 2023/9/15 13:17
     */
    public RecordSet getSet(String CQL, HashMap<String,Object> parameters){
        Session session= driver.session();
        Result result;
        try{
            result=runAndLog(session,CQL,parameters);
            return new RecordSet(result);
        }
        catch (Exception e){
            log.error(e.getMessage());
            log.error("cql错误,CQL:"+CQL+"参数:"+ JSONObject.toJSONString(parameters));
            throw new Error("执行语句错误");
        }
        finally {
            session.close();
        }
    }
    public RecordSet getSet(String CQL, Neo4jQueryWrapper queryWrapper){
        queryWrapper.build(0);
        CQL=CQL.replace(conditionPlaceholder," where "+queryWrapper.getFinalConditionCql())
                .replace(orderByPlaceholder,queryWrapper.getFinalOrderCql());
        replaceObjectCqlParameter(CQL,queryWrapper);
        return this.getSet(CQL,queryWrapper.getParameters());
    }
    private Result runAndLog(QueryRunner queryRunner,String cql,HashMap<String,Object> parameters){
        log.info("执行的语句："+cql);

        if(parameters==null||parameters.size()==0){
            return queryRunner.run(cql);
        }
        else {
            log.info("执行的参数"+JSONObject.toJSONString(parameters));
            return queryRunner.run(cql,parameters);
        }
    }

    private String replaceObjectCqlParameter(String CQL, Neo4jQueryWrapper neo4jQueryWrapper){
        Pattern pattern=Pattern.compile(this.objectCqlPlaceholder);
        Matcher m=pattern.matcher(CQL);
        while (m.find()){
            String str=m.group();
            String str1=recoverBrace(str);
            String[] arr=str1.split("\\.");
            String param=arr[arr.length-1];
            CQL=CQL.replace(str,neo4jQueryWrapper.getObjectCql().get(param));
        }
        return CQL;
    }
    /**
     * 去掉${}
     *
     * @return
     */
    private String recoverBrace(String str) {
        //转义字符要使用\\转义或使用"\\\\" +
        String recoverStr = str.replaceAll("\\$\\{","" ).replaceAll("}", "");
        return recoverStr;
    }
//    public RecordSet getSet(String CQL,Neo4jQueryWrapper queryWrapper,String countParameter,Integer page,Integer limit){
//        if(countParameter==null){
//            countParameter="*";
//        }
//        Integer index=StringUtils.countMatches(CQL.toLowerCase(),"return");
//        if(index.equals(-1)){
//            throw new Error("未找到return关键字");
//        }
//        StringBuilder stringBuilder=new StringBuilder(CQL);
//        stringBuilder.delete(index,CQL.length());
//        stringBuilder.append(" return count(");
//        stringBuilder.append(countParameter);
//        stringBuilder.append(") as count");
//        queryWrapper.build(null);
//        RecordSet result=getSet(stringBuilder.toString(),queryWrapper.getParameters());
//        Integer total= result.getRecords().get(0).get("count").asInt();
//        RecordSet queryResult=getSet(CQL,queryWrapper.getParameters());
//        return new RecordSet(queryResult,page,limit,total);
//    }
    @Data
    public static class RecordSet{
        private List<Record> records;
        private Integer page;
        private Integer limit;
        private Integer total;
        public RecordSet(Result result){
            records=result.list();
        }
        public RecordSet(Result result,Integer page,Integer limit,Integer total){
            records=result.list();
            this.page=page;
            this.limit=limit;
            this.total=total;
        }
        @SneakyThrows
        public <T> List<T> toList(String parameterName, Class<T> tClass){
            NodeInfo nodeInfo=new NodeInfo(tClass);
            List<T> list=new ArrayList<>();
            Set<Object> ids=new HashSet<>();
            for(Record record : records){
                T entity=tClass.newInstance();
                Value value=record.get(parameterName);
                if(value==null){
                    continue;
                }
                else {
                    convertNode(value,nodeInfo,entity);
                    convertRelation(value,nodeInfo,entity);
                    //Path path=value.asPath();
                    //org.neo4j.driver.types.Node node=value.asNode();
                    //org.neo4j.driver.types.Relationship relationship=value.asRelationship();
                }
                Object key=nodeInfo.getPrimaryField().get(entity);
                if(!ids.contains(key)){
                    list.add(entity);
                }
                ids.add(key);

            }
            return list;
        }
        public Neo4jModel toNeo4jModel(){
            Neo4jModel neo4jModel=new Neo4jModel();
            Set<Long> nodeIds=new HashSet<>();
            Set<Long> relationIds=new HashSet<>();
            for(Record record : records){
                for(Value value:record.values()){
                    Class<?> cla=value.getClass();
                    if(cla==org.neo4j.driver.internal.value.NodeValue.class){
                        org.neo4j.driver.types.Node node= value.asNode();
                        addNode(neo4jModel,node,nodeIds);
                    }
                    else if(cla==org.neo4j.driver.internal.value.RelationshipValue.class){
                        org.neo4j.driver.types.Relationship relation= value.asRelationship();
                        addRelation(neo4jModel,relation,relationIds);
                    }
                    else if(cla==org.neo4j.driver.internal.value.PathValue.class){
                        org.neo4j.driver.types.Path path=value.asPath();
                        Iterable<org.neo4j.driver.types.Relationship> relationships= path.relationships();
                        for(org.neo4j.driver.types.Node node:path.nodes()){
                            addNode(neo4jModel,node,nodeIds);
                        }
                        for (org.neo4j.driver.types.Relationship relationship:path.relationships()){
                            addRelation(neo4jModel,relationship,relationIds);
                        }
                    }
                }
            }
            return neo4jModel;
        }

        private void addNode(Neo4jModel neo4jModel,org.neo4j.driver.types.Node node,Set<Long> nodeIds){
            if(!nodeIds.contains(node.id())){
                Neo4jNode neo4jNode=new Neo4jNode();
                neo4jNode.setId(node.id());
                neo4jNode.setLabel((Collection<String>) node.labels());
                neo4jNode.setProperty(node.asMap());
                neo4jModel.nodes.add(neo4jNode);
                nodeIds.add(node.id());
            }

        }
        private void addRelation(Neo4jModel neo4jModel,org.neo4j.driver.types.Relationship relation,Set<Long> relationIds){
            if(!relationIds.contains(relation.id())){
                Neo4jRelation neo4jRelation=new Neo4jRelation();
                neo4jRelation.id=relation.id();
                neo4jRelation.startId=relation.startNodeId();
                neo4jRelation.endId=relation.endNodeId();
                neo4jRelation.type=relation.type();
                neo4jRelation.property=relation.asMap();
                neo4jModel.relations.add(neo4jRelation);
                relationIds.add(relation.id());
            }

        }
        @SneakyThrows
        private void convertNode(Value value, NodeInfo nodeInfo, Object entity){
            org.neo4j.driver.types.Node node;
            try{
                node=value.asNode();
            }
            catch (Exception e){
                return;
            }
            setValue(node,nodeInfo,entity);
            if(nodeInfo.getPrimaryField().get(entity)==null){
                Long id=node.id();
                nodeInfo.getPrimaryField().set(entity,convertNumberValue(id,nodeInfo.getPrimaryField().getType()));
            }
        }
        @SneakyThrows
        private void convertRelation(Value value, NodeInfo nodeInfo, Object entity){
            org.neo4j.driver.types.Relationship relationship;
            try{
                relationship=value.asRelationship();
            }
            catch (Exception e){
                return;
            }
            setValue(relationship,nodeInfo,entity);
            if(nodeInfo.getPrimaryField().get(entity)==null){
                Long id=relationship.id();
                nodeInfo.getPrimaryField().set(entity,convertNumberValue(id,nodeInfo.getPrimaryField().getType()));
            }
            if(nodeInfo.getStart()!=null){
                nodeInfo.getStart().set(entity,convertNumberValue(relationship.startNodeId(),nodeInfo.getStart().getType()));
            }
            if(nodeInfo.getEnd()!=null){
                nodeInfo.getEnd().set(entity,convertNumberValue(relationship.startNodeId(),nodeInfo.getEnd().getType()));
            }
            if(nodeInfo.getType()!=null){
                nodeInfo.getType().set(entity,convertNumberValue(relationship.startNodeId(),nodeInfo.getType().getType()));
            }
        }
        @SneakyThrows
        private void setValue(Entity entity, NodeInfo nodeInfo, Object object){
            Map<String,Object> valueMap=entity.asMap();
            for(Map.Entry<Field,String> entry : nodeInfo.getHashMap().entrySet()){
                if(valueMap.containsKey(entry.getValue())){
                    Object val=valueMap.get(entry.getValue());
                    if(Number.class.isAssignableFrom(val.getClass())){
                        val=convertNumberValue((Number)val,entry.getKey().getType());
                    }
                    entry.getKey().set(object,val);
                }
            }
        }

        private Object convertNumberValue(Number value,Class<?> targetClass){
            if(targetClass.equals(Integer.class)||targetClass.equals(int.class)){
                return value.intValue();
            }
            else if(targetClass.equals(Long.class)||targetClass.equals(long.class)){
                return value.longValue();
            }
            else if(targetClass.equals(Double.class)||targetClass.equals(double.class)){
                return value.doubleValue();
            }
            else if(targetClass.equals(Float.class)||targetClass.equals(float.class)){
                return value.floatValue();
            }
            else if(targetClass.equals(Short.class)||targetClass.equals(short.class)){
                return value.shortValue();
            }
            else if(targetClass.equals(String.class)){
                return value.toString();
            }
            else {
                return 0;
            }
        }
    }
    @Data
    public static class Neo4jModel{
        private List<Neo4jNode> nodes=new ArrayList<>();
        private List<Neo4jRelation> relations=new ArrayList<>();
    }
    @Data
    public static class Neo4jNode{
        private Collection<String> label;
        private Long id;
        private Map<String,Object> property;
    }
    @Data
    public static class Neo4jRelation{
        @NotNull(message = "id不能为空！")
        private Long id;
        @NotNull(message = "开始节点id不能为空")
        private Long startId;
        @NotNull(message = "结束节点id不能为空")
        private Long endId;
        @NotNull(message = "关系类型不能为空")
        private String type;
        private String typeName;
        private Map<String,Object> property;
    }
    @Data
    public static class NodeInfo{
        private NodeType nodeType;
        private HashMap<Field,String> hashMap=new HashMap<>();
        private Field primaryField;
        private Field start;
        private Field end;
        private Field type;
        public NodeInfo(Class<?> tClass){
            getNodeType(tClass);
            getFieldMap(tClass);
        }
        private void getNodeType(Class<?> tClass){
            Node node=tClass.getAnnotation(Node.class);
            Relationship relationship=tClass.getAnnotation(Relationship.class);
            if(node!=null){
                nodeType=NodeType.Node;
            }
            else if(relationship!=null){
                nodeType=NodeType.Relation;
            }
            else {
                nodeType=NodeType.UnKnow;
            }
        }
        private void getFieldMap(Class<?> tClass){
            Field[] fields=tClass.getDeclaredFields();
            for(Field field :fields){
                field.setAccessible(true);
                Property property=field.getAnnotation(Property.class);
                if(nodeType==NodeType.Relation){
                    RelationshipId relationshipId=field.getAnnotation(RelationshipId.class);
                    if(relationshipId!=null){
                        primaryField=field;
                    }
                    StartId startId=field.getAnnotation(StartId.class);
                    if(startId!=null){
                        start=field;
                    }
                    EndId endId=field.getAnnotation(EndId.class);
                    if(endId!=null){
                        end=field;
                    }
                    Type type=field.getAnnotation(Type.class);
                    if(type!=null){
                        this.type=field;
                    }
                }
                else {
                    Id id=field.getAnnotation(Id.class);
                    if(id!=null){
                        primaryField=field;
                    }
                }

                if(property!=null&& StringUtils.isNotBlank(property.name())){
                    hashMap.put(field,property.name());
                }
                else {
                    hashMap.put(field,field.getName());
                }
            }
        }
    }
    @Data
    public static class CQLGroup{
        private String CQL;
        private HashMap<String,Object> parameter;
        private Neo4jQueryWrapper neo4jQueryWrapper;
    }
    public enum NodeType{
        UnKnow,
        Node,
        Relation
    }
}